package org.sampleSpringFramework.util;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

@Slf4j
public class ClassUtil {


	public static final String FILE_PROTOCOL = "file";

	/**
	 * 获取指定包路径下的所有类
	 * @param packageName
	 * @return
	 */
	public static Set<Class<?>> extractPackageClass(String packageName){
		// 虽然通过文件IO流 + .class文件的绝对路径 方式也可以获取到指定包路径下的所有类， 但是不同机器的绝对路径不同，所以采用以下方案
		//1、获取业务系统的类加载器
		ClassLoader classLoader = getClassLoader();
		//2.通过类加载器获取到加载的资源
		// file:/D:/software/source_code/spring-framework-5.1.x/my-springframework/target/classes/com/gill
		URL url = classLoader.getResource(packageName.replace(".", "/"));
		if (url == null){
			log.warn("unable to retrieve anything from package: " + packageName);
			return  null;
		}
		//3.获取满足条件：本地的、.class结尾的资源
		Set<Class<?>> classSet = null;
		if(url.getProtocol().equalsIgnoreCase(FILE_PROTOCOL)){
			classSet = new HashSet<>();
			// D:/software/source_code/spring-framework-5.1.x/my-springframework/target/classes/com/gill
			File packageDirectory = new File(url.getPath());
			// 获取path下的所有.class文件
			extractClassFile(classSet, packageDirectory, packageName);
		}
		return classSet;
	}

	/**
	 * 递归获取目标package里面的所有class文件(包括子package里的class文件)
	 *
	 * @param emptyClassSet 装载目标类的集合
	 * @param fileSource    文件或者目录
	 * @param packageName   包名
	 * @return 类集合
	 */
	/**
	 * 递归获取目标package里面的所有class文件(包括子package里的class文件)
	 *
	 * @param emptyClassSet 装载目标类的集合
	 * @param fileSource    文件或者目录
	 * @param packageName   包名
	 * @return 类集合
	 */
	private static void extractClassFile(Set<Class<?>> emptyClassSet, File fileSource, String packageName) {
		if(!fileSource.isDirectory()){
			return;
		}
		//如果是一个文件夹，则调用其listFiles方法获取文件夹下的文件或文件夹
		File[] files = fileSource.listFiles(new FileFilter() {
			@Override
			public boolean accept(File file) {
				if(file.isDirectory()){
					return true;
				} else{
					//获取文件的绝对值路径
					String absoluteFilePath = file.getAbsolutePath();
					if(absoluteFilePath.endsWith(".class")){
						//若是class文件，则直接加载
						addToClassSet(absoluteFilePath);
					}
				}
				return false;
			}
			//根据class文件的绝对值路径，获取并生成Class对象，并放入classSet中
			private void addToClassSet(String absoluteFilePath) {
				//1.从class文件的绝对值路径里提取出包含了package的类名
				//如D:\software\source_code\spring-framework-5.1.x\my-springframework\target\classes\com\gill\controller\frontend\MainPageController.class
				//需要弄成com.gill.controller.frontend.MainPageController
				absoluteFilePath = absoluteFilePath.replace(File.separator, ".");
				String className = absoluteFilePath.substring(absoluteFilePath.indexOf(packageName));
				className = className.substring(0, className.lastIndexOf("."));
				//2.通过反射机制获取对应的Class对象并加入到classSet里
				Class targetClass = loadClass(className);
				emptyClassSet.add(targetClass);
			}
		});
		if(files != null){
			for(File f : files){
				//递归调用
				extractClassFile(emptyClassSet, f, packageName);
			}
		}
	}

	/**
	 * 获取当前项目的ClassLoader
	 * @return
	 */
	public static  ClassLoader getClassLoader(){
		return Thread.currentThread().getContextClassLoader();
	}

	/**
	 * 获取Class对象
	 *
	 * @param className class全名=package + 类名
	 * @return Class
	 */
	public static Class<?> loadClass(String className){
		try {
			return Class.forName(className);
		} catch (ClassNotFoundException e) {
			log.error("load class error:", e);
			throw new RuntimeException(e);
		}
	}

	/**
	 * 实例化class
	 *
	 * @param clazz Class
	 * @param <T>   class的类型
	 * @param accessible   是否支持创建出私有class对象的实例
	 * @return 类的实例化
	 */
	public static <T> T newInstance(Class<?> clazz,boolean accessible){
		// 此处声明为 <T> T泛型方法、和返回值为Object效果一致
		try {
			Constructor<?> constructor = clazz.getDeclaredConstructor();
			constructor.setAccessible(accessible);
			return (T)constructor.newInstance();
		} catch (Exception e) {
			log.error("newInstance error", e);
			throw new RuntimeException(e);
		}
	}

	/**
	 * 设置类的属性值
	 *
	 * @param field      成员变量
	 * @param target     所属类实例
	 * @param value      成员变量的值
	 * @param accessible 是否允许设置私有属性
	 */
	public static void setField(Field field, Object target, Object value, boolean accessible){
		field.setAccessible(accessible);//设置private属性不用进行安全检查
		try {
			field.set(target, value);
		} catch (IllegalAccessException e) {
			log.error("setField error", e);
			throw new RuntimeException(e);
		}
	}

	// 该类的缺陷，暂时只能获取自定义的类的.class文件，如果自定义类依赖于其他类，如实现/继承其他接口，则会classNotFound错误。
	public static void main(String[] args){
		Set<Class<?>> classes = extractPackageClass("com.gill");
		System.out.println(classes);
	}


}
